/*
 Created by lustre on 2023/10/3.
*/
#include "LightUdpClient.h"

#if !defined(HEARTBEAT_INTERVAL) || HEARTBEAT_INTERVAL < 1
#define HEARTBEAT_INTERVAL 60
#endif

// cmd字段
const char *CMD_ID = "id";
const char *OP_CODE = "op";
// MessageDigest长度 现在是MD5 16
const int MD_LEN = 16;
const uint8_t LOGIN_RESULT_HEADER[] = {'L', 'R', 'E', 'S', 'T'};
const uint8_t HELLO_ACK[] = {'L', 'H', 'A', 'C', 'K'};

const int maxLoginInfoSize = 32;

IPAddress LightUdpClient::getServerHost() {

    // 加入组，接收来自上位机的组播
    client.beginMulticast(IPAddress(224, 0, 0, 5), 8000);
    serial_info("get server ip...");
    const int topicLen = 12;
    const uint8_t TOPIC[12] = {'l', 'i', 'g', 'h', 't', '-',
                               's', 'e', 'r', 'v', 'e', 'r'};

    // 当上位机开热点的时候 大概率组播路由不会走它自己的ap的网段，这里向热点ip网关发送信息，
    // 如果对方是上位机，收到当前包 ，就往当前地址发包 来确认身份
    uint8_t client_topic[12] = {'l', 'i', 'g', 'h', 't',
                                '-', 'c', 'l', 'i', 'e', 'n', 't'};
    const IPAddress &gwIp = WiFi.gatewayIP();
    client.setServer(gwIp, AS_GATE_WAY_PORT);
    while (true) {
        int len = client.recv(rxBuffer, topicLen);
        if (len >= topicLen) {
            // 先尝试接受组播，
            if (std::equal(std::begin(TOPIC), std::end(TOPIC), std::begin(rxBuffer))) {
                const IPAddress &address = client.getRecRemoteIp();
                Serial.println();
                serial_infof("remote ip is:%u.%u.%u.%u\n", address[0], address[1], address[2], address[3]);
                client.endMulticast();
                return address;
            }
        }
        // 用于无组播时尝试询问网关是否是light server
        client.send(client_topic, 12);
        Serial.print("\r.");
        delay(400);
        Serial.print("\r..");
    }
}

inline int LightUdpClient::readUntil(uint8_t terminator) {
    int lastIndex = readIndex;
    for (int i = 0; readIndex < messageLength; readIndex++, i++) {
        uint8_t value = rxBuffer[readIndex];
        // 分隔符本身不作为值截取，索引就是值的长度。当前值需要跳过。
        if (value == terminator) {
            readIndex++;
            return i;
        }
        resultBuffer[i] = (char) value;
    }
    readIndex = lastIndex;
    return -1;
}

inline bool LightUdpClient::checkKey(int length, const char *keyName) {
    if (length < 1) {
        return false;
    }
    const auto &key = String(resultBuffer, length);
    return key == keyName;
}

inline void LightUdpClient::sendAction(uint8_t **action, const size_t *sizeTab, size_t subCount) {
    size_t len = subCount;
    for (int i = 0; i < subCount; ++i) {
        len += sizeTab[i];
    }
    uint8_t sendBuffer[len];
    for (size_t i = 0, sendIndex = 0; i < subCount; ++i) {
        size_t i1 = sizeTab[i];
        size_t subLen = i1;
        sendBuffer[sendIndex++] = subLen;
        memcpy(&sendBuffer[sendIndex], action[i], subLen);
        sendIndex += subLen;
    }
    client.send(sendBuffer, len);
}

inline void LightUdpClient::dropErrorInfo(const uint8_t *info, size_t len) {
    if (len < 1) {
        return;
    }
    if (client.getRecRemotePort() == AS_GATE_WAY_PORT) {
        return;
    }
    Serial.printf("dropErrorInfo ->length is [%d], info is:\n", len);
    Serial.write(info, len);
    Serial.println();
    Serial.print("hex:\n");
    for (int i = 0; i < len; ++i) {
        Serial.print(info[i], HEX);
    }
    Serial.println();
}

void LightUdpClient::connectWifi(const char *ssid, const char *passphrase) {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, passphrase);
    serial_info("connecting to router... ");
    // 等待wifi连接成功 连不上把调试日志打开
    while (WiFi.status() != WL_CONNECTED) {
        Serial.print(".");
        delay(500);
    }
    const IPAddress &localIp = WiFi.localIP();
    const IPAddress &gatewayIp = WiFi.gatewayIP();
    serial_infof("\nWiFi connected, local IP address:%u.%u.%u.%u\nWifi GWip is:%u.%u.%u.%u\n",
                 localIp[0], localIp[1], localIp[2], localIp[3],
                 gatewayIp[0], gatewayIp[1], gatewayIp[2], gatewayIp[3]);

}

bool LightUdpClient::login(const char *userName, const char *passwd) {
    serial_info("doLogin");
    uint8_t nonceHeader[] = {'n', 'o', 'n', 'c', 'e'};
    // 获取nonce
    uint8_t nonceLen;
    size_t nonceHeaderLen = sizeof(nonceHeader);
    uint8_t *getNonceRequest[]{nonceHeader, mac};
    size_t getNonceRequestSize[]{nonceHeaderLen, MAC_LEN};
    do {
        serial_info("try get nonce");
        sendAction(getNonceRequest, getNonceRequestSize, 2);
        size_t len = recvByTimes(3, 50, maxLoginInfoSize);

        // nonce 至少一位 header长度 1位 nonce长度1位 nonce 1位 也就是至少 + 3
        if (len < (nonceHeaderLen + 3)
            // 比较header
            || rxBuffer[0] != nonceHeaderLen
            || memcmp(nonceHeader, &rxBuffer[1], nonceHeaderLen) != 0
            // 判断nonce长度是否合法
            || (rxBuffer[nonceHeaderLen + 1] + nonceHeaderLen + 2) < len) {
            if (client.getRecRemotePort() != AS_GATE_WAY_PORT) {
                Serial.printf("lenCheck:%d,headerCheck:%d ,rxBuffer[nonceHeaderLen + 1]:%d ",
                              rxBuffer[0] == nonceHeaderLen, memcmp(nonceHeader, &rxBuffer[1], nonceHeaderLen),
                              rxBuffer[nonceHeaderLen + 1]);
                dropErrorInfo(rxBuffer, len);
            }
            delay(200);
            continue;
        }
        nonceLen = rxBuffer[nonceHeaderLen + 1];
        break;
    } while (true);
    // 认证的摘要 md5(nonce + md5(userName + passwd))
    uint8_t md[MD_LEN];
    MD5Builder md5Builder{};
    md5Builder.begin();
    md5Builder.add(userName);
    md5Builder.add(passwd);
    md5Builder.calculate();
    md5Builder.getBytes(md);

    md5Builder.begin();
    md5Builder.add(&rxBuffer[nonceHeaderLen + 2], nonceLen);
    md5Builder.add(md, MD_LEN);
    md5Builder.calculate();
    md5Builder.getBytes(md);

    // login request
    uint8_t loginHeader[] = {'l', 'o', 'g', 'i', 'n'};
    size_t userNameLen = strlen(userName);
    uint8_t *loginRequest[] = {loginHeader, mac, (uint8_t *) userName, md};
    size_t loginRequestSize[]{sizeof(loginHeader), MAC_LEN, userNameLen, MD_LEN};
    // response len
    size_t loginResultHeaderLen = sizeof(LOGIN_RESULT_HEADER);
    size_t loginResultLength = loginResultHeaderLen + 1;

    for (int i = 0; i < 3; ++i) {
        serial_info("try login");
        sendAction(loginRequest, loginRequestSize, 4);
        size_t len = recvByTimes(3, 50, loginResultLength);
        if (len >= loginResultLength && memcmp(LOGIN_RESULT_HEADER, rxBuffer, loginResultHeaderLen) == 0) {
            return rxBuffer[loginResultHeaderLen] == 0;
        }
        dropErrorInfo(rxBuffer, len);
        delay(200);
    }
    return false;
}


void LightUdpClient::helloServer(const uint16_t attrCount, const char *devAttr) {
    uint8_t helloHeader[] = {'h', 'e', 'l', 'l', 'o'};
    size_t headerLen = sizeof(helloHeader);
    size_t attrLen = strlen(devAttr);
    // headerLen header[headerLen] macLen mac[macLen] attrCount_2u attr
    size_t length = headerLen + 1 + MAC_LEN + 1 + 2 + attrLen;
    uint8_t sendBuffer[length];
    size_t index = 0;
    sendBuffer[index++] = headerLen;
    memcpy(&sendBuffer[index], helloHeader, headerLen);
    index += headerLen;
    sendBuffer[index++] = MAC_LEN;
    memcpy(&sendBuffer[index], mac, MAC_LEN);
    index += MAC_LEN;
    sendBuffer[index++] = attrCount & 0xff >> 8;
    sendBuffer[index++] = attrCount & 0xff;
    memcpy(&sendBuffer[index], (void *) devAttr, attrLen);

    size_t HELLO_ACK_LEN = sizeof(HELLO_ACK);
    for (int i = 0; i < 3; ++i) {
        serial_info("send hello");
        client.send(sendBuffer, length);
        size_t len = recvByTimes(5, 100, HELLO_ACK_LEN + 1);
        if (len < HELLO_ACK_LEN) {
            dropErrorInfo(rxBuffer, len);
            delay(200);
            continue;
        }
        serial_info("received info from the server");
        if (memcmp(HELLO_ACK, rxBuffer, HELLO_ACK_LEN) == 0) {
            serial_info("login success");
            return;
        }
        dropErrorInfo(rxBuffer, len);
        delay(200);
    }
    reStartApp("No ACK received from the server");
}

int LightUdpClient::readMsgHeader(MsgHeader *msgHeader) {
    readIndex = 0;
    messageLength = client.recv(rxBuffer, MTU_SIZE);
    if (messageLength < 1) {
        return MSG_FAILED;
    }
    int length = readUntil(':');
    if (!checkKey(length, CMD_ID)) {
        serial_error("checkKey CMD_ID failed drop it");
        dropErrorInfo(rxBuffer, messageLength);
        return MSG_FAILED;
    }
    length = readUntil('\n');
    if (length < 1) {
        serial_error("CMD_ID value");
        dropErrorInfo(rxBuffer, messageLength);
        return MSG_FAILED;
    }
    auto cmdIdVal = String(resultBuffer, length).toInt();
    length = readUntil(':');
    if (!checkKey(length, OP_CODE)) {
        serial_error("checkKey OP_CODE failed drop it");
        dropErrorInfo(rxBuffer, messageLength);
        return MSG_FAILED;
    }
    length = readUntil('\n');
    if (length < 1) {
        serial_error("OP_CODE value");
        dropErrorInfo(rxBuffer, messageLength);
        return MSG_FAILED;
    }
    auto opCode = String(resultBuffer, length).toInt();
    msgHeader->cmdIdVal = cmdIdVal;
    msgHeader->opCode = opCode;
    return MSG_OK;
}

IPAddress LightUdpClient::connectServer(ConnectInfo *connectInfo) {
    serial_info("light client version:"
                        LIGHT_UDP_VERSION
    );
    connectWifi(connectInfo->ssid, connectInfo->passphrase);
    WiFi.macAddress(mac);
    WiFi.macAddress(&keepRequest[6]);
    serial_infof("mac is:%02X%02X%02X%02X%02X%02X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
    IPAddress address((uint32_t) 0);
    if (connectInfo->host == nullptr) {
        serial_info("Server host is not specified");
        address = getServerHost();
    } else {
        serial_infof("Server host is specified as :%s\n", connectInfo->host);
        if (!(WiFi.hostByName(connectInfo->host, address))) {
            serial_error("get host by name failed");
            return address;
        }
    }
    client.setServer(address, connectInfo->serverPort);
    if (!login(connectInfo->userName, connectInfo->passwd)) {
        reStartApp("login failed!");
    }
    helloServer(connectInfo->attrCount, connectInfo->devAttr);
    return address;
}

/**
 * 由于非阻塞的接收太快收不到，这里加上延时和重试
 * */
inline int LightUdpClient::recvByTimes(uint8_t times, uint8_t delayMills, size_t length) {
    int len = 0;
    for (int i = 0; i < times; ++i) {
        delay(delayMills);
        len = this->client.recv(rxBuffer, length);
        if (len > 0) {
            // 重试次数
            serial_infof("The number of times to retry receiving the message:%d\n", i);
            break;
        }
    }
    return len;
}

int LightUdpClient::readInt(uint8_t terminator) {
    int length = readUntil(terminator);
    if (length < 1) {
        serial_error("read eof");
        return 0;
    }
    return String(resultBuffer, length).toInt();
}

String LightUdpClient::readStringUntil(uint8_t terminator) {
    int length = readUntil(terminator);
    if (length < 1) {
        serial_error("read eof");
        return "";
    }
    return {resultBuffer, static_cast<unsigned int>(length)};
}

void LightUdpClient::reStartApp(const char *failedCause) {
    serial_errorf("%s\nWill reset in three seconds.", failedCause);
    delay(3 * 1000);
    ESP.restart();
}

void LightUdpClient::startHeartbeat() {
#ifdef LIGHT_HEARTBEAT
    unsigned long now = millis();
    if (now  > (lastHeartbeatTime + HEARTBEAT_INTERVAL * 1000)) {
        log_i("send heartbeat");
        client.send(keepRequest, sizeof(keepRequest));
        lastHeartbeatTime = now;
    }
#endif
}
